8.5 TruJS - A simple AngularJS-like data-view binding library

  1. Motivations
    • Data-view binding in AngularJS, T3PO, and React
    • Do you remember how you can include PHP code in HTML code in the server side?
    • Can you include programming code in HTML code in the client side?

  2. TruJS - A simple AngularJS-like data-view binding JavaScript library
    • What to do - requirements
      • For simplicity, one TruJS app in an html application
      • Initialization of TruJS variables (data)
      • Modelling of input values (view) to TruJS variables (data), not <textarea>. Just <input> elements of 'text' type.
      • Binding of TruJS variables in a TruJS expression in <span> (view). You should remember that expressions are like JS expressions.
    • Here is an example app that uses TruJS. Try it! You will see what needs to be implemented.

    • Implementation idea:
      • $trujs defined in the library
      • Three user-defined attributes in HTML elements:
        • trujs-init:
          • TruJS variables will be created with initial values.
          • Only in <span>
          • The value of this attribute is like a JavaScript expression that can be evaluated only once.
          • E.g., <span trujs-init="$trujs.a = 20; $trujs.b = 'Wow!'"></span> -> TruJS variables, a and b, will be created in $trujs as properties with the initial values. $trujs.a -> 20 and $trujs['b'] -> 'Wow!'.
        • trujs-model:
          • TruJS variables will be created.
          • Only in <input>
          • All the TruJS variables start with '$trujs.', i.e., properties in $trujs.
          • E.g., <input type='text' trujs-model='x' value='10'> -> The input value will be modelled by $trujs.x with the initial value 10.
        • trujs-bind:
          • Only in <span> for the convenience of implementation
          • This attribute is used to list which TruJS variables are used in a TruJS expression.
          • E.g., <span trujs-bind='a, b, c'> let d = 10; $trujs.a++; d + ", " + $trujs.b + ", " + $trujs.c </span> -> The TruJS expression, i.e., the content of <span>...</span>, will be initially evaluated and re-evaluated whenever there is any change in any one of a, b, and c.
          • If trujs-bind='' -> The TruJS expression will be evaluated only once in the beginning.
      • TruJS expressions:
        • Nothing but JS expressions; {{...}} not used
        • Only in <span> that includes trujs-bind for the convenience of implement
        • E.g., <span trujs-bind=...> let d = 10; $trujs.a++; d + " " + $trujs.b + " " + $trujs.c + " " + $trujs.x; </span>
      • Summary:
        • Two attributes, trujs-init and trujs-model, are used to describe TruJS variables (i.e., model).
        • TruJS variables will be created in $trujs as properties.
        • The attribute, trujs-bind, is used to describe which TruJS variables are bound to an output <span>.
        • The content of an output <span> is a JavaScript code.
        • <input trujs-init='x'> -> $trujs.x (or $trujs['x']) in JavaScript -> multiple <span trujs-bind='x...'>

    • How to implement?
      • Let's study closely the code in the left-side pane of example app that uses TruJS. Here is the code.
        <script src="https://cs.tru.ca/~mlee/comp4620/Software/TruJS.mo.js"></script>
        
        <span trujs-init='$trujs.b = 20'></span>
        <span trujs-init=''>$trujs.c = 30</span>
        Case 1: <input type='text' trujs-model='x' value=''><br>
        Case 2: <input type='text' trujs-model='y' value='TruJS'><br>
        Case 3: <input type='text' trujs-model='z' value='Fun'><br>
        <br>
        <div>
        Wow!<br><br>
        Case 1: <span trujs-bind='x'>$trujs.x</span><br> <!-- '$trujs.x' to be evaluated whenever trujs.x is updated. -->
        Case 2.1: <span trujs-bind='y'></span><br> <!-- no expression -->
        Case 2.2: <span trujs-bind=''>$trujs.y</span><br> <!-- '$trujs.y' to evaluated only once because trujs-bind does not include y. -->
        Case 2.3: <span >$trujs.y</span><br> <!-- no evaluation because trujs-bind is not defined. -->
        Cases 1 and 3: <span trujs-bind='x, z'>let a = 10; $trujs.b++; a + ", " + $trujs.b + ", " + $trujs.c + ", " + $trujs.x + ", " + $trujs.z.toUpperCase()</span><br>
        </div>
        
        <!-- 
        The next code is used just for testing. 
        Without it, Trial 1 would not work, because the TruJS library scans all the elements only with the 'onload' event on 'window'.
        -->
        <script> $trujs_init(); </script>
        
      • Trial 1: Let's try the above example, with the following.


      • Trial 1.5: Let's try another simpler example. (Note that this trial might not work if Trial 1 was already executed. This is because when <span> is evaluated with $trujs_init(), if there is a syntax error, then JavaScript stops.)



      • Idea:
        • Data structures:
          $trujs = {};  // models and values; each model --> value; E.g., let model = 'a'; $trujs[model] = 20;
          $trujs._expressions = {};  // models and expressions; each model --> linear array of objects; 
                                     //   each object, {output:..., expr:...}, includes 
                                     //     output: span element object, and 
                                     //     expr: JS expression that is the content of the span element
                                     // E.g., $trujs._expressions[model].push({output:..., expr:...});  
          // E.g., $trujs['a'] -> 20;
          //       $trujs._expressions['a'] -> [{output:spanobj, expr:'$trujs.a += 20;'}, ...]
          
        • After initialization,
        • When there is a change(, i.e., 'keyup',) in a TruJS input, obtain the model name from trujs-model='m'.
              Update $trujs['m'] with the new value.
              For i = 0 to $trujs._expressions['m'].length - 1, evaluate $trujs._expressions['m'][i].expr, and
                  assign the result to $trujs._expressions['m'][i].output.innerHTML.

      • Steps to implement
        1. Collection of models and initialization with trujs-init in <span>s
          • $trujs
          • $trujs._expressions
        2. Collection of models with trujs-model in <input>s
          • $trujs
          • $trujs._expressions
        3. Collection of models and expressions with trujc-bind in <span>s
          • $trujs._expressions
        4. Initial evaluation of expressions
          • $trujs._expressions
        5. Registration of 'keyup' event listeners on <input>s that include trujs-model. Update of the related model value, and evaluation of expressions related to the model.
          • $trujs
          • $trujs._expressions

      • Exercise - Initialization: How are TruJS variables implemented? With inputs and outputs?
        • TruJS variables will become properties in a global variable, $trujs, in the TruJS library.
        • Trial 2: Let's create some TruJS variables(, i.e., models,) directly.



        • Trial 2.1: Let's read TruJS variables from <span>s with trujs-init and save them in $trujs. Let's evaluate the expression in <span> that uses the two TruJS variables. How? eval()



        • Trial 2.2: Let's read TruJS variables from <input>s with trujs-model and save them in $trujs.



        • Trial 2.4: Let's collect expressions from the <span>s that have trujs-bind.



      • Exercise - Evaluation of expressions: How are TruJS expressions evaluated?
        • TruJS expressions will be stored in $trujs, and they will be evaluated by using 'eval()'.
        • Trial 3: Let's read a TruJS expression, evaluate it and display the result. Note that two models a and b are collected in $trujs in the previous Trial.



      • Exercise - Registration of 'keyup' event listeners:
        • Inside the event listener,
              Get the model, e.g., m;
              Get the new value and update $trujs[m];
              For each expression in $trujs._expressions[m], evaluate expression and update the output;
        • Trial 4: Let's add the step of initial evaluation of expressions in $trujs._expressions.



        • Trial 5: Let's add event listeners for the <input>s that have trujs-model. Test the data-view binding by entering something in the <input>s.


      • So far, we have used hard-coded model <input>s, init <span>s, and bind <span>s. Can we select them using .querySelectorAll() and use?

      • Actual implementation: How to search TruJS inputs and outputs?
        • document.querySelectorAll() with attribute selectors
        • Trial 6: Let's search TruJS inputs and outputs within <div id='tr6-app'>. Test the data-view binding by entering something in <input>s.



        • Trial 6.1: Step 1. Select the <span>s that include trujs-init, and initialize TruJS models



        • Trial 6.2: Step 2. Select the <input>s that include trujs-model, and initialize models



        • Trial 6.3: Step 3. Select the <span>s that include trujs-bind, and add expressions to their corresponding models



        • Trial 6.4: Step 4. Initial evaluation - Evaluate expressions, and update outputs, i.e., <span>s that have trujs-bind



        • Trial 6.5: Step 5. Select the <input>s that include trujs-model, and register 'keyup' event listeners. Type a number in <input>s and see the output <span> is changed together. (Note that the output <span> may not be evaluated if Step 5 has not been completed.)



      • [If time permits] - It is not convenient to include $trujs in trujs-init and in expressions. How to remove $trujs from trujs-init and expressions in bind <spans>? For examples, <span trujs-init='d = 23; e=45'></span> and <span trujs-bind = 'd'>a + b + c + d</span>.
        Any good idea?
        • Tricks for trujs-init
          function $trujs_update() 
          {
              // Step1: Select the <span>s that include trujs-init, and initialize TruJS models
              let init_spans = document.querySelectorAll("span[trujs-init]");
              for (let i = 0; i < init_spans.length; i++) {
                  let init_models = init_spans[i].getAttribute("trujs-init").trim().split(/; */);   
                  for (let j = 0; j < init_models.length; j++) {
                      if (!init_models[j].startsWith("_expressions")) {
                          let str = "$trujs." + init_models[j].trim();  // Ahha! "$trujs." is attached.
                          eval(str);
                      }
                  }
              }        
              for (let p in $trujs) {
                  if (p != "_expressions")  // i.e., a model
                      $trujs._expressions[p] = [];
              }
          }
          

        • Tricks for expressions (not perfect though; what if a model is changed in an expression?)
          function $trujs_update() 
          {
              // Step 4. Initial evaluation
              //         The same idea in Step 5 can be used.
              
              // Step 5. Select the <input>s that include trujs-model, and register 'keyup' event listeners
              //         In the event listener, update the model, evaluate the related expressions, and update the related s.
              let model_inputs = document.querySelectorAll("input[trujs-model]");
              for (let i = 0; i < model_inputs.length; i++) {
                  model_inputs[i].addEventListener('keyup', function() {
                      let model = this.getAttribute('trujs-model').trim();
                      $trujs[model] = ????
                      for (let j = 0; j < $trujs._expressions[model].length; j++) {
                          let output_span = ????
                          let expr = "{";
                          for (let p in $trujs)
                              if (p != "_expressions")
                                  expr += `let ${p} = '${$trujs[p]}';`;  // Ahha! New local variables with all the models in $trujs
                          expr += $trujs._expressions[model][j].expr;  // Ahha! The variables here will use those declared above.
                          expr += "}";
                          ????  // upate the output  with the evaluation result
                      }
                  });
              }
          }
          

  3. Learning outcomes
    • Analyze the concept of data-view binding
    • Develop a library that supports data-view binding